/* VIEW and CONTROLLER of the Model-View-Controller
 */

package textselection;

import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.*;
import java.awt.geom.*;
import javax.swing.border.EtchedBorder;  // netbeans said I needed this (even though above import...)

/**
 * @author Jason Frank
 */
public class TextView extends JComponent implements TextView_Interface {

  protected TextSelectionModel model;
  protected final int sideMargin = 10; // pixels
  protected final int topMargin  = 10;
  protected final int betweenLinesPix = 10;
  protected Font fontObj;
  // place in a line where a cursor will be drawn (if x is valid). Gets set to -1 if not valid.
  protected int cursorIndex = -1;
  protected int line_number = -1;  // Initialize to "invalid" (between lines, etc)
  protected boolean cursorValid = false; // used to determine if key events should do anything
  protected int lineX;  // x val of the cursor drawn. Need this for moving cursor one pos
  // # pixels from R/L of str that are valid for a cursor in a line
  protected int left_threshold = 0, right_threshold = 5;

  // constructor
  public TextView(TextSelectionModel model) {

    this.model = model;
    this.model.addObserver(this);
    fontObj = new Font( Font.SERIF, Font.BOLD, model.getStrPointSize());
    //Graphics g = getGraphics();
    //g.setFont(fontObj);  // CAN'T CALL THIS HERE. SOMETHING MUST BE VISIBLE FOR THE
                           // GRAPHICS OBJECT NOT TO BE NULL (DO IT IN getPreferredSize()
                           // See: http://www.coderanch.com/t/407987/java/java/getGraphics-returns-null
                           // HOWEVER: I can (and must) call these in a helper func (below). I have to
                           //  call these below so that I get correct stringWidth measurements
                           //  (based on the user's point size, rather than the defualt point size).

    // Run the controller code (register event listeners, etc)
    this.controllerInit();

  } /// end constructor ///


  /// CONTROLLER code //////////////
  private void controllerInit() {

    addMouseListener(new MouseAdapter() {
      @Override
      public void mousePressed( MouseEvent event ) {
        // change x,y in model, model notifies views, curValChanged() calls repaint
        model.setXY(event.getX(), event.getY());
        // System.err.println("mousePressed handler has been called.");
      }
      // The following method is needed in order for ANY KEYTYPED/PRESSED events to be generated!
      // requestFocusInWindow() gives a componenet KEYBOARD FOCUS. 
      @Override
      public void mouseEntered(MouseEvent e) {
        // get the object that generated the event
        JComponent obj = (JComponent)e.getSource();
        obj.requestFocusInWindow(); // see comments above about why this needs to be called
      }
    });

    addKeyListener(new KeyAdapter() {
      @Override
      public void keyPressed(KeyEvent e) {

        if(cursorValid) {
          int keyCode = e.getKeyCode();
          final int leftArrow  = KeyEvent.VK_LEFT;
          final int rightArrow = KeyEvent.VK_RIGHT;
          final int lower_a    = KeyEvent.VK_A;
          final int lower_e    = KeyEvent.VK_E;
          final int del        = KeyEvent.VK_DELETE;
          final int backspace  = KeyEvent.VK_BACK_SPACE;
          final int enter      = KeyEvent.VK_ENTER;

          switch(keyCode) {
            case leftArrow:
              System.err.println("Looks like you PRESSED a LEFT ARROW!");
              move_cursor_one_pos("Left");
              break;
            case rightArrow:
              System.err.println("Looks like you PRESSED a RIGHT ARROW!");
              move_cursor_one_pos("Right");
              break;
            case lower_a:
              System.err.println("Looks like you PRESSED a LOWERCASE A...next will see if CTRL was held.");
              // Only act if CTRL was held when user pressed "a" (but not Shift or Alt)
              if( e.isControlDown() && !(e.isShiftDown() || e.isAltDown()) ) {
                System.err.println("I see you held CTRL when you pressed LOWERCASE A");
                move_cursor_beg_line();
              } 
              break; 
            case lower_e:
              System.err.println("Looks like you PRESSED a LOWERCASE e...next will see if CTRL was held.");
              // Only act if CTRL was held when user pressed "e" (but not Shift or Alt)
              if( e.isControlDown() && !(e.isShiftDown() || e.isAltDown())) {
                System.err.println("I see you held CTRL when you pressed LOWERCASE E");
                move_cursor_end_line();
              }
              break; 
            case del:
              System.err.println("You pressed a DELETE!");
              delete_char();
              break;
            case backspace:
              System.err.println("You pressed a BACKSPACE!");
              delete_char();
              break;

            case enter:  // Return key
              System.err.println("You pressed a RETURN Key!");
              resize_window(e);
              break;
            default:
              break;
          }
        } // end if cursorValid
      } /////// end overriding keyPressed() ////

      // Handle inserting any PRINTABLE CHAR
      @Override
      public void keyTyped(KeyEvent e) {
        if(cursorValid) {
          System.err.println("Looks like you TYPED a: " + e.getKeyChar());
          // If the key typed is a printable char...
          if(! Character.isISOControl(e.getKeyChar())) {
            insert_char(e.getKeyChar());
          }
        } // end if cursorValid
      } /// end overriding keyTyped() /////////
    }); // end addKeyListener()
  } /////// end controllerInit() /////////

  // Moves the cursor one position (unless its already at head of the str/end of line)
  protected void move_cursor_one_pos(String direction) {

    // line_number will be valid since this func only called when cursor is valid
    String str = (model.getStrs())[line_number];
    FontMetrics f = this.reset_Graphics_Font();  // must reset the Font again, or it reverts back to default
    int old_cursor_x = this.lineX;  // prev line drawn

    if( direction.equals("Left")) {
      // only change x val if cursor is not already at far left position
      if(old_cursor_x <= this.sideMargin) {
      }
      else{
        int new_x;
        int charWidth_left_of_cursor = f.stringWidth(str.substring(cursorIndex -1, cursorIndex));
        new_x = old_cursor_x - charWidth_left_of_cursor;

        model.setXY(new_x, model.getY());  // model calls curValChanged(), which calls repaint()
      }
    }
    else if(direction.equals("Right")) {
      // only change x val if cursor is not already at far right position
      if(old_cursor_x >= (f.stringWidth(str) + this.sideMargin)) {
      }     
      else{
        int new_x;
        int charWidth_right_of_cursor = f.stringWidth(str.substring(cursorIndex, cursorIndex +1));
        new_x = old_cursor_x + charWidth_right_of_cursor;

        model.setXY(new_x, model.getY());  // model calls curValChanged(), which calls repaint()
      }
    }
    else {
      System.err.println("\nERROR: move_cursor_one_pos() called with unknown direction param: " + direction);
      System.err.println("         direction must be either \"Left\" or \"Right\"");
      System.exit(-1);
    }
  } //// end move_cursor_one_left() /////


  protected void move_cursor_beg_line()  {
    model.setXY(this.sideMargin, model.getY());
  } ////////// end move_cursor_beg_line() ////////

  protected void move_cursor_end_line()  {

    FontMetrics f = this.reset_Graphics_Font();
    String str = model.getStrs()[this.line_number]; // line_number will be valid
    int new_x = this.sideMargin + f.stringWidth(str) + this.right_threshold -1;
    
    model.setXY(new_x, model.getY());
  } ////////// end move_cursor_beg_line() ////////

  // Delete the char currently to the left of the cursor (a model method actually del the char)
  protected void delete_char() {

    String str = model.getStrs()[this.line_number];
    FontMetrics f = this.reset_Graphics_Font();
    int new_x = this.lineX;
    // If the cursor is not already at beg of line...
    if(this.lineX != this.sideMargin) {
      // then move the cursor line one char to the left (width of the deleleted char)
      new_x -= f.stringWidth(str.substring(cursorIndex -1, cursorIndex));
      // delete the char before the current cursor
      model.delete_char(this.line_number, this.cursorIndex -1);
      model.setXY(new_x, model.getY());
    }
  } ////////// end delete_char() ///////////

  // Insert a char before the current cursor
  protected void insert_char( char c )  {
    FontMetrics f = this.reset_Graphics_Font();
    String new_char_str = new String();
    new_char_str = String.valueOf(c);
    // make new cursor be to the right of the inserted char
    int new_x = this.lineX + f.stringWidth(new_char_str);
    // Insert the char into the actual string in the model
    model.insert_char(this.line_number, new_char_str, this.cursorIndex);
    model.setXY(new_x, model.getY());
  } ///////// end insert_char() ////////////

  // When user hits "enter" resize the window to fit the updated strings
  protected void resize_window(KeyEvent e) {

    // Mark this JComponent as "invalid" so that the next time pack() gets called on the parent,
    //  it will have to call this obj's getPreferredSize() and the window size will get resized.
    revalidate(); 
    // Now I must call pack() on the JFrame that this JComponent is in (so first get the JFrame)
    // Adapted next 2 lines from: http://www.exampledepot.com/egs/javax.swing/frame_FrameFind.html
    Component c = (Component) e.getSource();  // get the object that caused the event
    // Get the JFrame of the component
    JFrame frame = (JFrame) SwingUtilities.getRoot(c);
    frame.pack();
  } //// end resize_window() //////


  // For some reason I have to always reset the Graphics context with the Font I need
  //  (or it reverts back to default font... wrong size)
  protected FontMetrics reset_Graphics_Font() {
    Graphics g = getGraphics();
    g.setFont(fontObj);  // must reset the Font again, or it reverts back to default
    FontMetrics f = g.getFontMetrics();
    return f;
  } ////////// end reset_Graphics_Font() /////////


  @Override
  public void paintComponent(Graphics g) {
    System.err.println("paintComponent() has been called.");
        // System.err.println("in paintComponent(), font point size: "+ g.getFont().getSize());
    super.paintComponent(g);
    // for some reason I must set this again here (did it in getPreferredSize too)
    g.setFont(fontObj);
    Graphics myG = g.create();
    Graphics2D g2 = (Graphics2D) myG;
        //this.setBorder(BorderFactory.createLineBorder(Color.BLACK));

        // System.err.println("in paintComponent(), after calling setFont(), font point size: "+ myG.getFont().getSize());
    String strs[] = model.getStrs();
    int y;
    FontMetrics f = myG.getFontMetrics();
    //////////////////////////////////////
    // Draw the strings //////////////////
    for(int i = 0; i < strs.length; i++ ) {
      y = (i+1) * (f.getHeight() + betweenLinesPix);
      myG.drawString(strs[i], sideMargin, y);
    }
    ///////////////////////////////////////////////
    // Find which LINE # the (x,y) point is at /////
    line_number = -1;  // Initialize to "invalid" (between lines, etc)
    
    // next line corresponds to BVZ's: line_nu = (y - top)/(height + leading);
    line_number = (model.getY() - this.topMargin) /
                  (f.getHeight() + this.betweenLinesPix);
    // Set line# to invalid if its actually between lines
    // next line corresponds to BVZ's: if (y-top) > ((height + leading)*(line_nu +1) -leading)
    if( (model.getY() - this.topMargin) >
        ((f.getHeight() + betweenLinesPix) * (line_number +1) - betweenLinesPix)) {
      line_number = -1;  // between lines is "invalid" line number
    }
    // handle special case of y being above first line
    if( model.getY() < this.topMargin ) { line_number = -1; }
    // handle case of y being below last line
    int last_line_bottom = (f.getHeight() + this.betweenLinesPix) * model.getNumStrs()
                            + this.topMargin;
    if( model.getY() > last_line_bottom ) { line_number = -1; }
    
    System.out.println("\n----------------------------");
    System.out.println("Line#: " + line_number);
    /////////////////////////////////////////////
    // Find the "cursor index" of the (x,y) point
    //
    // only try to find it if we have a valid line number
    if( line_number >= 0 ) {
      // make sure x falls within a reasonable distance of the text where threshold is the
      //  amount by which x can be outside the textbox
      this.cursorIndex = 0;
      int left = this.sideMargin;
      int right = f.stringWidth(strs[line_number]) + this.sideMargin;
      int x = model.getX();
      // If to the left of the line, or more than 5 pix from right of line, invalid
      if( (x < (left - left_threshold)) || (x > (right + right_threshold)) )
        cursorIndex = -1;  // mark invalid

      // Only proceed to calculate and draw cursor if x is within proper line bounds
      if(cursorIndex != -1) {
        // translate x so that it is relative to the start of the textbox
        x = x - left;
        // At end of loop, cursor_index will be at the position before the char of the x point
        for (cursorIndex = 0; cursorIndex < strs[line_number].length(); cursorIndex++) {
          int substrWidth = f.stringWidth(strs[line_number].substring(0, cursorIndex +1));
          if (x < substrWidth )
            break;
        }
        System.out.println( "CursorIndex: " + cursorIndex);
        // will need to add sideMargin to cursor_pixels to get x val of the cursor to be drawn
        int cursor_pixels = f.stringWidth(strs[line_number].substring(0, cursorIndex));
        System.out.println( "Cursor_pixels: " + cursor_pixels);
        ///////////////////////////////////
        // Draw the cursor ///////////////
        this.lineX = cursor_pixels + this.sideMargin;
        int startY = line_number * (f.getHeight() + this.betweenLinesPix) + this.topMargin;
        int endY   = startY + f.getHeight();
        g.drawLine(lineX, startY, lineX, endY);
      } // end if the x is within proper bounds of the line
    } // end if we have a valid line_number

    // set cursorValid so that key events know if they should do anything
    if( line_number < 0 || cursorIndex < 0) { this.cursorValid = false; }
    else { this.cursorValid = true; }

    myG.dispose();
  } ///// end paintComponent() ////

  public void curValChanged(int old_x, int old_y) {
    repaint();  // zero-arg verion of repaint() repaints entire contentPane
  } ///// end curValChanged() ///


  @Override
  public Dimension getMinimumSize()   { 
    return this.getPreferredSize();
  }
  
  @Override
  public Dimension getPreferredSize() {
    int maxWidth = 0, windowWidth, height = 0;
    Graphics g = getGraphics();
    g.setFont(this.fontObj);   // need to set this here so correct height/width can be calc'd
    FontMetrics f = g.getFontMetrics();
    String strs[] = model.getStrs();
    // Find max width of all strs
    for (String str: strs) {
      if( f.stringWidth(str) > maxWidth ) { maxWidth = f.stringWidth(str);  }
    }
    windowWidth = maxWidth + (2 * sideMargin);  // not working correctly?
    height = (f.getHeight() + this.betweenLinesPix) * model.getNumStrs()
              + this.topMargin + this.topMargin;  // 10 pix margin top and bottom
    
    return new Dimension(windowWidth, height);
  } /// end getPreferredSize() ////////
} ///////////////// end class TextView ///////////////////////////////
